package com;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.ns.basics.commons.lang.NumberUtils;

import com.alibaba.fastjson.JSON;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class MyBatisDemo {
	public static void main(String[] args) {
		HikariConfig hikariConfig = new HikariConfig();
		hikariConfig.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/funbit?useunicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true&serverTimezone=Asia/Shanghai");
		hikariConfig.setUsername("root");
		hikariConfig.setPassword("123456");
		hikariConfig.setMaximumPoolSize(5);
		hikariConfig.setMinimumIdle(2);
		Environment environment = new Environment("test", new JdbcTransactionFactory(), new HikariDataSource(hikariConfig));
		
		Configuration conf = new Configuration(environment);
		// conf.getTypeHandlerRegistry().register(null)
		conf.addMapper(TestMapper.class);
		conf.addInterceptor(new SplitTableIntercept());
		SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(conf);
		
		SqlSession s =  sessionFactory.openSession();
		TestMapper testMapper = s.getMapper(TestMapper.class);
		System.out.println(JSON.toJSONString(testMapper.selectVal(8, 100), true));
		s.close();
	}
	
	@Intercepts({@Signature(args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}, method = "query", type = Executor.class)})
	public static class SplitTableIntercept implements Interceptor {
		private static Field field = null;
		private static Map<String, List<FormatSql>> sqlCaches = new ConcurrentHashMap<String, List<FormatSql>>();
		static {
			try {
				field = MappedStatement.class.getDeclaredField("sqlSource");
				field.setAccessible(true);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		@Override
		public Object intercept(Invocation invocation) throws Throwable {
			MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
			ParamMap<Object> param = (ParamMap<Object>) invocation.getArgs()[1];
			BoundSql boundSql = mappedStatement.getBoundSql(param);
			String sql = boundSql.getSql();
			if (sql.endsWith("#HASH")) {
				sql = formatSql(sql, param);
				StaticSqlSource sqlSource = new StaticSqlSource(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings());
				Field field = MappedStatement.class.getDeclaredField("sqlSource");
				field.setAccessible(true);
				field.set(mappedStatement, sqlSource);
			}
			return invocation.proceed();
		}
		
		private String formatSql(String sql, ParamMap<Object> param) {
			List<FormatSql> formatSqls = sqlCaches.get(sql);
			if (formatSqls == null) {
				String optSql = sql.substring(0, sql.length() - "#HASH".length());
				formatSqls = new ArrayList<>();
				int recordIndex = 0;
				String hashStart = "[HASH ";
				while (true) {
					int startIndex = optSql.indexOf(hashStart, recordIndex);
					
					if (startIndex == -1) {
						if (recordIndex == 0) {
							formatSqls.add(new StaticSql(optSql));
						} else {
							formatSqls.add(new StaticSql(optSql.substring(recordIndex)));
						}
						break;
					}
					formatSqls.add(new StaticSql(optSql.substring(recordIndex, startIndex)));
					recordIndex = startIndex;
					int fakeIndex = startIndex + hashStart.length(); // [HASH |.... 假结束位置
					int endIndex = optSql.indexOf("]", fakeIndex);   // [HASH |....]   真结束位置
					if (endIndex == -1) {
						formatSqls.add(new StaticSql(optSql.substring(recordIndex, fakeIndex)));
						recordIndex = fakeIndex;
					} else {
						int modIndex = -1;
						for (int i = fakeIndex; i < endIndex; i++) {
							if (optSql.charAt(i) == '%') {
								modIndex = i;
								break;
							}
						}
						recordIndex = endIndex + 1; // 记录到最后到位置
						if (modIndex == -1) { // 没有发现取模运算符号
							formatSqls.add(new StaticSql(optSql.substring(startIndex, endIndex + 1)));
						} else {
							// 有取模运算符号
							String name = optSql.substring(fakeIndex, modIndex).trim(); // 获取取模运算名
							int mod = NumberUtils.toInt(optSql.substring(modIndex + 1, endIndex).trim()); // 取模运算数
							if (name.isEmpty() || mod <= 0) { // 取模运算得值错误得
								formatSqls.add(new StaticSql(optSql.substring(startIndex, endIndex + 1)));
							} else {
								formatSqls.add(new HashSql(name, mod));
							}
						}
					}
				}
				sqlCaches.put(sql, formatSqls);
			}
			
			
			StringBuilder sb = new StringBuilder(sql.length() + 100);
			for (FormatSql formatSql : formatSqls) {
				sb.append(formatSql.getValue(param));
			}
			return sb.toString();
		}
		
		private static interface FormatSql {
			Object getValue(ParamMap<Object> param);
		}
		private static class StaticSql implements FormatSql {
			String sql;
			public StaticSql(String sql) {
				super();
				this.sql = sql;
			}
			@Override
			public Object getValue(ParamMap<Object> param) {
				return sql;
			}
			
			@Override
			public String toString() {
				return sql;
			}
		}
		private static class HashSql implements FormatSql {
			private String name; // 做HASH的name
			private int mod;  // 取模运算
			
			public HashSql(String name, int mod) {
				this.name = name;
				this.mod = mod;
			}

			@Override
			public Object getValue(ParamMap<Object> param) {
				Object value = param.get(name);
				if (value instanceof Number) {
					return ((Number) value).longValue() % mod;
				}
				return value.hashCode() % mod;
			}
			@Override
			public String toString() {
				return "[HASH " + name + "%" + mod+ "]";
			}
		}
	} 
}
